// Copyright (c) 2022 Presto Labs Pte. Ltd.
// Author: lucasyoo

#pragma once

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <limits>
#include <list>
#include <numeric>
#include <string>
#include <unordered_map>
#include <vector>

#include <boost/cstdint.hpp>
#include <glog/logging.h>

#include "coin2/exchange/base/feed/update.h"
#include "coin2/exchange/base/feed_util/book_builder.h"
#include "presto/quant/math/ewma.h"
#include "presto/quant/math/sampled_moving_average.h"

namespace coin2::strategy::hamm {

using coin2::exchange::base::feed::FeedUpdate;
using presto::math::SampledMovingAverage;

int64_t DatetimeToUnixTimestamp(const int64_t& datetime);

bool IsClose(double a, double b);

bool IsZero(double a);

double LinearTimeDecay(double time_length, double x1, double y1, double y2);

struct VolatilityCalc {
  VolatilityCalc(
      int64_t window_sec,
      int64_t tick_intv_sec,
      double init_volatility,
      std::optional<double> volatility_thold = std::nullopt)
      : p_(window_sec * 1e9, 1e9),
        p2_(window_sec * 1e9, 1e9),
        tick_intv_sec(tick_intv_sec),
        init_volatility(init_volatility),
        volatility_thold(volatility_thold.has_value() ? volatility_thold.value() : init_volatility),
        sqrt_window_size_year(sqrt(static_cast<double>(3600 * 24 * 365) / window_sec)) {}

  void UpdatePrice(double price, int64_t fetched_time) {
    if (prev_update_time + tick_intv_sec * 1e9 < fetched_time) {
      p_.Update(fetched_time, price);
      p2_.Update(fetched_time, price * price);
      prev_update_time = fetched_time;
    }
  }

  double Volatility() const {
    if (!p_.popped()) return init_volatility;
    double mean = p_.average();
    if (p2_.average() - mean * mean <= 0) return init_volatility;
    if (mean <= 0) return init_volatility;
    return sqrt(p2_.average() - mean * mean) / mean * sqrt_window_size_year;
  }

  double Threshold() const { return volatility_thold; }

  bool IsVolatile() const { return Volatility() > volatility_thold; }

  SampledMovingAverage<double> p_, p2_;
  int64_t prev_update_time{0L};
  int64_t tick_intv_sec;
  double init_volatility;
  double volatility_thold;
  double sqrt_window_size_year;
};

struct MovingAverage {
  MovingAverage(int64_t window_sec, int64_t tick_intv_sec)
      : collector_(window_sec * 1e9, 1e9), tick_intv_sec_(tick_intv_sec * 1e9) {}

  void UpdateData(double d, int64_t fetched_time) {
    if (prev_update_time_ + tick_intv_sec_ < fetched_time) {
      collector_.Update(fetched_time, d);
      prev_update_time_ = fetched_time;
    }
  }

  double MA() const {
    if (!collector_.popped()) return 0.0;
    return collector_.average();
  }

  SampledMovingAverage<double> collector_;
  int64_t prev_update_time_{0L};
  int64_t tick_intv_sec_{};
};

struct TradeCollector {
  explicit TradeCollector(int64_t window_sec, std::optional<double> alpha = std::nullopt)
      : buy_q_(window_sec * 1e9),
        sell_q_(window_sec * 1e9),
        buy_pq_(window_sec * 1e9),
        sell_pq_(window_sec * 1e9),
        p_(window_sec * 1e9),
        buy_p_(window_sec * 1e9),
        sell_p_(window_sec * 1e9),
        ewma_buy_pq_(window_sec * 1e9, alpha),
        ewma_buy_q_(window_sec * 1e9, alpha),
        ewma_sell_pq_(window_sec * 1e9, alpha),
        ewma_sell_q_(window_sec * 1e9, alpha) {}

  void
  UpdateTrade(double p, double q, int64_t fetched_time, coin2::exchange::feed::TradeSide side) {
    if (side == coin2::exchange::feed::TradeSide::TRADE_BUY_SIDE) {
      buy_q_.Update(fetched_time, q);
      buy_pq_.Update(fetched_time, p * q);
      buy_p_.Update(fetched_time, p);
      ewma_buy_pq_.Update(fetched_time, p * q);
      ewma_buy_q_.Update(fetched_time, q);
    } else {
      sell_q_.Update(fetched_time, q);
      sell_pq_.Update(fetched_time, p * q);
      sell_p_.Update(fetched_time, p);
      ewma_sell_pq_.Update(fetched_time, p * q);
      ewma_sell_q_.Update(fetched_time, q);
    }
    p_.Update(fetched_time, p);
  }

  void UpdateTimeOnly(int64_t fetched_time) {
    for (auto* window : {&buy_q_, &buy_pq_, &sell_q_, &sell_pq_, &p_, &buy_p_, &sell_p_}) {
      window->UpdateTimeOnly(fetched_time);
    }
    for (auto* window : {&ewma_buy_pq_, &ewma_buy_q_, &ewma_sell_pq_, &ewma_sell_q_}) {
      window->UpdateTimeOnly(fetched_time);
    }
    prev_update_time_ = fetched_time;
  }

  double MA() const {
    if (p_.num_popped() == 0) return 0.0;
    return p_.average();
  }

  double VWAP() const {
    if ((buy_q_.num_popped() == 0 && sell_q_.num_popped() == 0) ||
        IsZero(buy_q_.sum() + sell_q_.sum()) ||
        buy_q_.num_elements() + sell_q_.num_elements() == 0 || buy_pq_.sum() + sell_pq_.sum() < 0.0)
      return 0.0;
    return (buy_pq_.sum() + sell_pq_.sum()) / (buy_q_.sum() + sell_q_.sum());
  }

  double BuyVWAP() const {
    if (buy_q_.num_popped() == 0 || buy_pq_.num_popped() == 0 || IsZero(buy_q_.sum()) ||
        buy_q_.num_elements() == 0 || buy_pq_.sum() < 0.0)
      return 0.0;
    return buy_pq_.sum() / buy_q_.sum();
  }

  double SellVWAP() const {
    if (sell_q_.num_popped() == 0 || sell_pq_.num_popped() == 0 || IsZero(sell_q_.sum()) ||
        sell_q_.num_elements() == 0 || sell_pq_.sum() < 0.0)
      return 0.0;
    return sell_pq_.sum() / sell_q_.sum();
  }

  double VolIntensity(const int& trade_cnt) const {
    if ((buy_q_.num_popped() == 0 && sell_q_.num_popped() == 0) ||
        buy_q_.sum() + sell_q_.sum() <= trade_cnt)
      return -1;

    return buy_q_.sum() / (buy_q_.sum() + sell_q_.sum());
  }

  double BuyQty() const {
    double res = buy_q_.sum();
    if (IsZero(res)) return 0.0;
    return res;
  }

  double SellQty() const {
    double res = sell_q_.sum();
    if (IsZero(res)) return 0.0;
    return res;
  }

  double BuyNum() const { return buy_q_.num_elements(); }
  double SellNum() const { return sell_q_.num_elements(); }

  double SellElapsedSec() const { return sell_elapsed_sec_; }
  double BuyElapsedSec() const { return buy_elapsed_sec_; }

  std::pair<double, double> BboFadePrice(
      const double& edge_bps,
      const double& decay_sec,
      const double& min_pos,
      const double& max_pos,
      const double& lot_size,
      const double& reserve,
      const double& pos,
      const double& fade_bps_multiplier,
      const int64_t& fill_num_hurdle) {
    std::pair<double, double> ret;
    ret.first = 0;
    ret.second = 0;
    double sell_edge_bps = edge_bps;
    double buy_edge_bps = edge_bps;
    if (fade_bps_multiplier != 0.0 && !sell_q_.empty()) {
      double sellq_sum = sell_q_.sum();
      if (!IsZero(sellq_sum)) {
        if (IsClose(pos, min_pos)) {
          sell_edge_bps *= fade_bps_multiplier;
        } else {
          sell_edge_bps *=
              std::clamp(
                  std::max(
                      std::pow(sellq_sum / lot_size, 2),
                      std::pow(sell_q_.num_elements() / static_cast<double>(fill_num_hurdle), 2)),
                  1.0,
                  std::sqrt(fade_bps_multiplier)) *
              std::clamp(
                  (reserve - min_pos) / (pos - min_pos),
                  1.0,
                  std::sqrt(fade_bps_multiplier));
        }
      }
    }
    if (!sell_p_.empty()) {
      double sellp = (sell_p_.end() - 1)->second;
      if (!IsZero(sellp)) {
        sell_elapsed_sec_ = 1e-9 * (prev_update_time_ - (sell_p_.end() - 1)->first);
        ret.second = sellp * (1 + 1e-4 * sell_edge_bps * std::exp(-sell_elapsed_sec_ / decay_sec));
      }
    }
    if (fade_bps_multiplier != 0.0 && !buy_q_.empty()) {
      double buyq_sum = buy_q_.sum();
      if (!IsZero(buyq_sum)) {
        if (IsClose(pos, max_pos)) {
          buy_edge_bps *= fade_bps_multiplier;
        } else {
          buy_edge_bps *=
              std::clamp(
                  std::max(
                      std::pow(buyq_sum / lot_size, 2),
                      std::pow(buy_q_.num_elements() / static_cast<double>(fill_num_hurdle), 2)),
                  1.0,
                  std::sqrt(fade_bps_multiplier)) *
              std::clamp(
                  (max_pos - reserve) / (max_pos - pos),
                  1.0,
                  std::sqrt(fade_bps_multiplier));
        }
      }
    }
    if (!buy_p_.empty()) {
      double buyp = (buy_p_.end() - 1)->second;
      if (!IsZero(buyp)) {
        buy_elapsed_sec_ = 1e-9 * (prev_update_time_ - (buy_p_.end() - 1)->first);
        ret.first = buyp * (1 - 1e-4 * buy_edge_bps * std::exp(-buy_elapsed_sec_ / decay_sec));
      }
    }
    return ret;
  }

  double EWMABuyVWAP() {
    if (ewma_buy_q_.num_popped() == 0 || ewma_buy_pq_.num_popped() == 0 ||
        IsZero(ewma_buy_q_.sum()) || ewma_buy_q_.num_elements() == 0 || ewma_buy_pq_.sum() < 0.0) {
      return 0.0;
    }
    return ewma_buy_pq_.expaverage2() / ewma_buy_q_.expaverage2();
  }

  double EWMASellVWAP() {
    if (ewma_sell_q_.num_popped() == 0 || ewma_sell_pq_.num_popped() == 0 ||
        IsZero(ewma_sell_q_.sum()) || ewma_sell_q_.num_elements() == 0 ||
        ewma_sell_pq_.sum() < 0.0) {
      return 0.0;
    }
    return ewma_sell_pq_.expaverage2() / ewma_sell_q_.expaverage2();
  }

  presto::math::TimeWindowMovingAverage<double> buy_q_{}, sell_q_{}, buy_pq_{}, sell_pq_{}, p_{},
      buy_p_{}, sell_p_{};
  presto::math::MovingAverageWindow<double> ewma_buy_pq_{}, ewma_buy_q_{}, ewma_sell_pq_{},
      ewma_sell_q_{};
  int64_t prev_update_time_{0L};
  double sell_elapsed_sec_{0L};
  double buy_elapsed_sec_{0L};
};

/*
class VolatilityCalculator {
 public:
  VolatilityCalculator(int64_t window_sec, int64_t tick_intv_sec, double init_volatility);

  void UpdatePrice(double midp, int64_t ts);

  void CalculateVolatility();

  // Getter
  double Volatility() const { return volatility_; }

 private:
  int64_t window_sec_{0};
  int64_t tick_intv_sec_{0};
  int64_t window_size_{0};
  int64_t last_update_ts_{0};
  double prev_midp_{0};

  double square_sum_{0};
  std::list<double> diffs_;
  double volatility_{0.0};
};
*/
}  // namespace coin2::strategy::hamm
